/*
 * This file is part of NativeAccessHooks.
 *
 * Copyleft 2014 Mark Jeronimus. All Rights Reversed.
 * https://code.google.com/p/nativeaccesshooks/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NativeAccessHooks. If not, see <http://www.gnu.org/licenses/>.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.digitalmodular.nah.keyboard;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import com.digitalmodular.nah.HookManager;
import com.digitalmodular.nah.NativeHook;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef.HINSTANCE;
import com.sun.jna.platform.win32.WinDef.LRESULT;
import com.sun.jna.platform.win32.WinDef.WPARAM;
import com.sun.jna.platform.win32.WinUser;
import com.sun.jna.platform.win32.WinUser.HHOOK;
import com.sun.jna.platform.win32.WinUser.HOOKPROC;
import com.sun.jna.platform.win32.WinUser.KBDLLHOOKSTRUCT;
import com.sun.jna.platform.win32.WinUser.LowLevelKeyboardProc;
import com.sun.jna.platform.win32.WinUser.MSG;

/**
 * The hook that receives platform-wide key messages. When a message is received, it's put into a queue with a timestamp attached. A
 * separate thread monitors the queue, formulates {@link NativeKeyEvent} events and dispatches them through
 * {@link HookManager#dispatchEvent(com.digitalmodular.nah.NativeInputEvent)};
 *
 * @author Mark Jeronimus
 * @since 1.0
 */
// date 2014-08-22
public class NativeKeyHook extends NativeHook implements Runnable {
	final LinkedBlockingQueue<KeyboardPostMessage>	buffer		= new LinkedBlockingQueue<>();

	HHOOK											hhk			= null;

	// Mutex:
	ReentrantLock									lock		= new ReentrantLock();
	Condition										condition	= lock.newCondition();

	public NativeKeyHook() {
		new Thread(getClass().getSimpleName() + " callback thread") {
			@Override
			public void run() {
				final HOOKPROC lpfn = new LowLevelKeyboardProc() {
					@Override
					public LRESULT callback(final int nCode, final WPARAM wParam, final KBDLLHOOKSTRUCT lParam) {
						if (nCode >= 0) {
							// System.out.println("wParam=" + Integer.toHexString(wParam.intValue()) + " vkCode="
							// + Integer.toHexString(lParam.vkCode) + " scanCode=" + Integer.toHexString(lParam.scanCode)
							// + " flags=" + Integer.toHexString(lParam.flags) + " time=" + lParam.time);

							lock.lock();
							try {
								buffer.offer(new KeyboardPostMessage(System.currentTimeMillis(), System.nanoTime(), wParam, lParam));
								condition.signalAll();
							}
							finally {
								lock.unlock();
							}
						}
						return User32.INSTANCE.CallNextHookEx(hhk, nCode, wParam, lParam.getPointer());
					}
				};

				synchronized (REGISTER_MUTEX) {
					final HINSTANCE hMod = Kernel32.INSTANCE.GetModuleHandle(null);

					hhk = User32.INSTANCE.SetWindowsHookEx(com.sun.jna.platform.win32.WinUser.WH_KEYBOARD_LL, lpfn, hMod, 0);

					REGISTER_MUTEX.notifyAll();

					if (hhk == null) {
						System.out.println("Failed to register " + NativeKeyHook.class.getSimpleName() + ".");

						return;
					}

					System.out.println(NativeKeyHook.class.getSimpleName() + " hook registered.");
				}

				try {
					int result;
					final MSG msg = new MSG();
					while ((result = User32.INSTANCE.GetMessage(msg, null, 0, 0)) != 0) {
						if (result == -1) {
							unregisterHook();
							return;
						}
						User32.INSTANCE.TranslateMessage(msg);
						User32.INSTANCE.DispatchMessage(msg);
					}
				}
				finally {
					unregisterHook();
				}
			}
		}.start();

		synchronized (REGISTER_MUTEX) {
			if (hhk == null) {
				try {
					REGISTER_MUTEX.wait();
				}
				catch (InterruptedException e) {}
			}
		}

		new Thread(this, getClass().getSimpleName() + " dispatch thread").start();
	}

	@Override
	public boolean isRegistered() {
		return hhk != null;
	}

	@Override
	public void unregisterHook() {
		User32.INSTANCE.UnhookWindowsHookEx(hhk);
		hhk = null;

		System.out.println(NativeKeyHook.class.getSimpleName() + " hook unregistered.");
	}

	private static class KeyboardPostMessage {
		public final long				time;
		public final long				nanoTime;
		public final WPARAM				wParam;
		public final KBDLLHOOKSTRUCT	lParam;

		public KeyboardPostMessage(final long time, final long nanoTime, final WPARAM wParam, final KBDLLHOOKSTRUCT lParam) {
			this.time = time;
			this.nanoTime = nanoTime;
			this.wParam = wParam;
			this.lParam = lParam;
		}
	}

	@Override
	public void run() {
		try {
			while (true) {
				lock.lock();
				try {
					condition.await();
				}
				finally {
					lock.unlock();
				}

				KeyboardPostMessage message;
				while ((message = buffer.poll()) != null) {
					long time = message.time;
					long nanoTime = message.nanoTime;
					int wParam = message.wParam.intValue();
					int scanCode = message.lParam.scanCode;
					int vkCode = message.lParam.vkCode;
					int flags = message.lParam.flags;

					if ((vkCode & 0x100) != 0 || (scanCode & 0x100) != 0) {
						StringBuilder stringBuilder = new StringBuilder("Unsupported key[");
						stringBuilder.append("scanCode=0x").append(Integer.toHexString(scanCode));
						stringBuilder.append(", vkCode=0x").append(Integer.toHexString(vkCode));
						stringBuilder.append(", flags=0b").append(Integer.toString(flags, 2));
						System.err.println(stringBuilder.append("]").toString());
						continue;
					}

					int extended = flags & 0x0001;
					int sup = scanCode & ~0x7F;

					// Apparently does nothing.
					if (sup != 0) {
						continue;
					}

					ScanCode logicalKey = ScanCode.values()[scanCode & 0x7F | extended << 7];
					// if (logicalKey == null) {
					// StringBuilder stringBuilder = new StringBuilder("Unknown key[");
					// stringBuilder.append("scanCode=0x").append(Integer.toHexString(scanCode));
					// stringBuilder.append(", vkCode=0x").append(Integer.toHexString(vkCode));
					// stringBuilder.append(", flags=0b").append(Integer.toString(flags, 2));
					// System.err.println(stringBuilder.append("]").toString());
					// continue;
					// }

					VirtualKey virtualKey = VirtualKey.values()[vkCode];

					switch (wParam) {
						case WinUser.WM_KEYDOWN:
						case WinUser.WM_SYSKEYDOWN:
							HookManager.dispatchEvent(new NativeKeyEvent(this, time, nanoTime,
									NativeKeyEvent.NativeKeyEventType.Pressed, logicalKey, virtualKey));
							break;
						case WinUser.WM_KEYUP:
						case WinUser.WM_SYSKEYUP:
							HookManager.dispatchEvent(new NativeKeyEvent(this, time, nanoTime,
									NativeKeyEvent.NativeKeyEventType.Released, logicalKey, virtualKey));
							break;
						default:
							StringBuilder stringBuilder = new StringBuilder(256);
							stringBuilder.append("Unknown key event[");
							stringBuilder.append("messageType=0x").append(Integer.toHexString(wParam));
							stringBuilder.append(", scanCode=0x").append(Integer.toHexString(scanCode));
							stringBuilder.append(", vkCode=0x").append(Integer.toHexString(vkCode));
							stringBuilder.append(", flags=0x").append(Integer.toHexString(flags));
							stringBuilder.append(", key=").append(logicalKey);
							System.err.println(stringBuilder.append("]").toString());
					}
				}
			}
		}
		catch (InterruptedException e) {}
	}
}
